--region NewFile_1.lua
--Author : zhoujibo
--Date   : 2015/5/6
--Desc : 支持富文本排版

--[[
支持的功能
    ubb代码转换和定制
    宏扩展
    标签类型扩展
    缓存功能
    水平对齐，垂直对齐，行对齐
    词组换行
    图片缩放
    图片错误显示
    过宽图片字符处理
    自动高度增加
    触发响应
计划实现的功能
    段落缩进
    运行性能统计
    辅助信息表示增强
当前不支持的功能
    基线对齐
    自动换行 见 http://zh.wikipedia.org/wiki/%E8%87%AA%E5%8A%A8%E6%8D%A2%E8%A1%8C
    行间距和字间距设定
    下划线,加粗,斜字体
    动态加载图片的设定
    动画
    卷动动态填充内容
]]

--[[
    UBB架构
    富文本字符串
    
    == step 1 setString ==
    ->标签文法分析和标签宏替换(UBBTagConvert.convertUBBString)

    ->得到语法树

    == step 2 setUBBAst ==
    
    ->根据标签定义(UBBStylePlugin)将语法树转换为元素列表(UBBTagConvert.convertUBBAstToElements)
    
    ->得到元素列表（语法树是嵌套结构，列表是单层结构，方便处理）

    == step 3 setElements  ==

    ->统计每个元素中的'单词'宽度 UBBElement.getElementWordInfo

    ->根据'单词'宽度信息和控件本身的行宽度，将每个元素中的单词内容拆分成'段' UBBElement.convertElementsToSegments
    
    == step 4 _setSegments == 

    ->段渲染，其中用到UBBCache进行缓存加速



]]


module('UBBLabel',package.seeall)



require 'common/ubb/UBBElement'
require 'common/ubb/UBBCache'
require 'common/ubb/UBBTagConvert'
require 'common/ubb/UBBUtil'
require 'common/ubb/UBBStylePlugin'

--todo 用系统内内已经有的对象替代
_ANCHOR_POINT = cc.p(0,0)



NONE_TOUCH = {}

DEBUG_DRAW_BORDER = true and DEBUG_VERSION
DEBUG_DRAW_SEGMENT = false and DEBUG_DRAW_BORDER
DEBUG_NOCACHE = false and DEBUG_VERSION
DEBUG_NOLINE_COMBINE = false and DEBUG_VERSION

UBBLabel = class("UBBLabel",function()
    local ret = cc.Node:create()
    ret:setName('UBBLabel')
    --控件宽度
    ret.width = nil
    --控件高度，没有则自动增长
    ret.height = nil
    --垂直方向对齐 默认靠上
    ret.vAlign = nil
    --水平方向对齐 默认靠左
    ret.hAlign = nil
    --临时自定义tag集
    ret.customTag = nil

    --原始字符串
    ret.originalStr=nil
    --原始字符串（空白）
    ret.originalPlainStr = nil
    --元素列表
    ret.elements = nil
    --存储元素和touchid的对应关系
    ret.touchDict = nil
    

    --每一行的总长度
    ret.heights = nil 
    --每一行的总宽度
    ret.widths = nil 
    --每一行的node数组
    ret.lineNodes = nil 
    --总高度
    ret.totalH = nil 
    --每一行的段宽度以及对应的touchid
    ret.lineSegTouchArray = {}
    
    --监听器的句柄
    ret.listener = nil
    --监听函数
    ret.callbacks = {}

    --debug层
    ret.DEBUG_layer = nil
    --content层
    ret.content_layer = nil

    --使用中的对象池
    ret.label_pool_in_use = nil
    ret.res_pool_in_use = nil


    
    return ret
end)


UBBLabel.__index = UBBLabel


function create()
    return createWithParams(100,100)
end

function createWithParams(width,height,hAlign,vAlign)
    local ret = UBBLabel.new()
    ret:init(width,height,hAlign,vAlign)
    return ret
end

function UBBLabel:getContentSize()
    return {width=self.width,height=(self.height or self.totalH)}
end

function UBBLabel:init(width,height,hAlign,vAlign)
    assert(width>0 and(height==nil or height>0),'size must have valid value!')
    self.width = width
    self.height = height
    self.hAlign = hAlign or cc.TEXT_ALIGNMENT_LEFT
    self.vAlign = vAlign or cc.VERTICAL_TEXT_ALIGNMENT_TOP
    if(self.height)then
        self:setContentSize(cc.size(self.width,self.height))
    end

    if DEBUG_VERSION then
        self.DEBUG_layer = cc.Node:create()
        self.content_layer = cc.Node:create()
        self:addChild(self.DEBUG_layer)
        self:addChild(self.content_layer)
    else
        self.content_layer = self
    end

    self.label_pool_in_use = {}
    self.res_pool_in_use = {}
    
end

function UBBLabel:addTouchListener(callback)
    for _,func in ipairs(self.callbacks)do
        if func == callback then
            return func
        end
    end
    table.insert(self.callbacks,callback)
    self:_enableTouch()
end

function UBBLabel:delTouchListener(callback)
    for i,func in ipairs(self.callbacks)do
        if func == callback then
            table.remove(self.callbacks,i)
            break
        end
    end
    if #self.callbacks==0 then
        self:_disableTouch()
    end
end

function UBBLabel:setTouchCallback(callback)
    if callback then
        self.callbacks = {callback}
        self:_enableTouch()
    else
        self.callbacks = {}
        self:_disableTouch()
    end
end

function UBBLabel:_getTouchID(x,y)
    for _,segTouchArray in ipairs(self.lineSegTouchArray)do
        if(segTouchArray[1]>=y and y>segTouchArray[2] and #segTouchArray>2)then
            for i = 3,#segTouchArray do
                --考虑一行内容高度不会相差太多的情况下，暂时只比较横向 zjb 2015年5月18日
                local touchInfo = segTouchArray[i]
                local nodeX = touchInfo[1]
                local nodeY = touchInfo[2]
                local nodeRight = touchInfo[3]
                local nodeTop = touchInfo[4]
                local touchID = touchInfo[5]
                print((nodeX<=x and x<nodeRight),x,nodeX,nodeRight)
                if(nodeX<=x and x<nodeRight)then
                    return touchID
                end
            end
            break
        end
    end
    return NONE_TOUCH
end

function UBBLabel:_enableTouch()
    if not self.listener then
        self.listener = cc.EventListenerTouchOneByOne:create()
        local touchID = nil
        self.listener:registerScriptHandler(function(touch,event)
            local p = self:convertToNodeSpace(touch:getLocation())
            touchID = self:_getTouchID(p.x,p.y)
            return true
        end,cc.Handler.EVENT_TOUCH_BEGAN) 
        self.listener:registerScriptHandler(function(touch,event)
            return true
        end ,cc.Handler.EVENT_TOUCH_MOVED)
        self.listener:registerScriptHandler(function(touch,event)
            if touchID~=NONE_TOUCH then
                local touchP = touch:getLocation()
                local p = self:convertToNodeSpace(touchP)
                local id2 = self:_getTouchID(p.x,p.y)
                if  touchID == id2 then
                    --如何判定点在可视范围之内呢？？ zjb 2015年5月18日
                    for _,callback in ipairs(self.callbacks)do
                        callback(touchID)
                    end
                end
            end
            return true
        end ,cc.Handler.EVENT_TOUCH_ENDED)
        local eventDispatcher = self:getEventDispatcher()
        eventDispatcher:addEventListenerWithSceneGraphPriority(self.listener,self)
    end
end

function UBBLabel:_disableTouch()
    if self.listener then
        self:getEventDispatcher():removeEventListener(self.listener)
        self.listener = nil
    end
end


--设置对齐参数
--@param horizon 水平对齐参数 nil表示用已有的参数， 可能的值，cc.TEXT_ALIGNMENT_LEFT/CENTER/RIGHT
--@param vertical 垂直对齐参数 nil表示用已有的参数， 可能的值，cc.VERTICAL_TEXT_ALIGNMENT_TOP/CENTER/BOTTOM
function UBBLabel:setAlign(horizon,vertical)
    horizon = horizon or self.hAlign
    vertical = vertical or self.vAlign
    if horizon~=self.hAlign or vertical~=self.vAlign then
        self.hAlign = horizon
        self.vAlign = vertical
        self:adjustPosition()
    end
end

function UBBLabel:setSize(width,height)
    width = width or self.width
    height = height or self.height
    if width~=self.width or height~=self.height then
        self.width = width
        self.height = height
        self:reloadData()
    end
end

function UBBLabel:reloadData()
    self:setElements(self.elements,self.touchDict)
end

function UBBLabel:_setCustomTag(tags)
    if tags then
        if tags.type then
            self.customTag = {c=tags}
        else
            self.customTag = tags
        end
    else
        self.customTag = nil
    end
    
end

--设置文本内容,带ubb转换
--格式[tag,param1,param2]content[/tag]
--或[tag,param1,param2/]
--tag和param部分不要包含[和空格,content部分[用[[转义
--基础标签
--类别1：文字类标签
--[1]xxxxx[/1] 如果是数字对应label的style样式，其他见相关自定义
--类别2：资源定位标签，
--[类别 参数1 参数2...]资源位置[/类别]
--默认支持的资源定位标签
--img: 对应图片路径名 参数1 scaleX(1)参数2 scaleY(1)
--二级标签 自定义标签
--[tag] 或 [tag]xxx[/tag] 或[tag 参数][/tag]
--如果tag内容：既不在数字类标签中，也不属于资源定位标签，则从customUBBTag中查找
--如果写成[]或[]xxx[/] 则认为customUBBTag为标签定义对象，而不是标签集
--保留的标签：1-n,g,img,c,text,li,s,touch

function UBBLabel:setString(s,customUBBTag)
    if self.originalString ~= s then
        self.originalString = s
        self.originalPlainString = nil
        if customUBBTag then
            self.customTag =(customUBBTag.type and {c=customUBBTag})or customUBBTag
        else
            self.customTag = nil
        end
        local ast = UBBTagConvert.convertUBBString(s,self.customTag)
        self:_setUBBAst(ast)
    end
end

--设置纯字符，带默认风格
function UBBLabel:setPlainString(s)
    if self.originalPlainString ~= s then
        self.originalPlainString = s
        self.originalString = nil
        self:_setUBBAst({type='tag',name='default',body={{type='text',text=s}}})
    end
end



function UBBLabel:_setUBBAst(root)
    local elements,touchDict = UBBTagConvert.convertUBBAstToElements(root,self.customTag)
    self:setElements(elements,touchDict)
end











--将元素根据行宽转换为段
--元素类型说明
--[类型,参数...]
--[[文字类[1=UBBElement.TYPE_TEXT,
    2:文字内容,
    3:字体(DEFAULT_TTF_PATH),
    4:字体大小(20),
    5:颜色(ARGB)cc.c4b(255,255,255,255),
    6:描边颜色cc.c4b(255,255,255,255),
    7:描边宽度(0)]
]]
--资源类[UBBElement.TYPE_RES,资源类别,资源位置,[资源参数数组]]
--  其中资源图片类： 图片路径
--使用图片的前提是图片必须要提前加载
--  计数器类：风格|计数器文字串
--组合类[UBBElement.TYPE_GROUP,[元素列表]]
--占位类[UBBElement.TYPE_PLACEHOLDER,宽度,高度(0)]
--分行符类[UBBElement.TYPE_LI_START]会根据接下来的元素内容影响start和end之间的分行情况，只有在elements->segments
--的时候才能确定之间插入的内容
--[UBBElement.TYPE_LI_END]
--缩放类[UBBElement.TYPE_SCALE_START,sX,sY][UBBElement.TYPE_SCALE_END]，在elements->segments时确定分行的情况
--交互类[UBBElement.TYPE_TOUCH_START,touchid][UBBElement.TYPE_TOUCH_END],在setsegment阶段分配touchid
--段落类[UBBElement.TYPE_PARA_START,水平对齐(left)][UBBElement.TYPE_PARA_END]
--行对齐类[UBBElement.TYPE_LINEALIGN_START,行对齐(bottom)][UBBElement.TYPE_LINEALIGN_END]
function UBBLabel:setElements(elements,touchDict)
    self.elements = elements
    self.touchDict = touchDict or {}
    
    --local t = os.clock()
    local wordsinfo = UBBElement.getElementWordInfo(elements)
    --print(string.format("elapsed time: %f",os.clock()- t))

    local segments = UBBElement.convertElementsToSegments(elements,wordsinfo,self.width)

    self:_setSegments(segments,wordsinfo)
    
    self:adjustPosition()
end



--直接设置段显示，段是已经分好行的数据结构，因此超出部分不会进行换行
--段在容器中铺好后，将根据对齐参数校准位置
--请按照行组进行段数据组织{{行1段1，行1段2...},{行2段1，行2段2...}...}
--段结构
--{元素引用,开始索引,终止索引,转换的内容,sX,sY,pAlign,pVAlign}
--元素引用结构查看 UBBLabel:setElements()注释
--如果要构成只有换行没有实际内容的行，使用元素引用中有\n的字符索引，或将开始索引设置为-1
function UBBLabel:_setSegments(lines,wordsinfo)
    if DEBUG_NOCACHE then
        --如果不使用对象池，清空缓存
        self.label_pool_in_use = {}
        self.res_pool_in_use = {}
    end
    local oldList = self.content_layer:getChildren()
    local newList = {}
    

    local heights = {} --每一行的总长度
    local widths = {} --每一行的总宽度
    local lineNodes = {} --每一行的node数组(node,，宽，高，水平对齐，行对齐，touchID
    local totalH = 0 --总高度
    local maxW = 0


    local LAZY_PLACE = {} --占位用
    local LAZY_PLACE_RES = {}

    local lazyNodes = {}--延后加载的项目

    local new_label_pool = {}
    local new_res_pool = {}

    local elementWidthsDict = {}
    local elementHeightsDict = {}
    for _,arr in ipairs(wordsinfo)do
        elementWidthsDict[arr[1]] = arr[3]
        elementHeightsDict[arr[1]] = arr[4]
    end


    for _,segments in ipairs(lines)do
        local x = 0
        local h = 0
        local nodes = {}
        for _,segment in ipairs(segments)do
            local element = segment[1]
            local segStartIndex =  segment[2] or 1
            local segEndIndex = segment[3] or 1
            local sX = segment[5] or 1
            local sY = segment[6] or 1
            local pAlign = segment[7]
            local lineAlign = segment[8]
            assert(segStartIndex<=segEndIndex,'wrong segment index')
            local eType = element[1]
            local touchID = self.touchDict[element] or NONE_TOUCH
            local charWidths = elementWidthsDict[element]
            local charHeights = elementHeightsDict[element]
            if eType == UBBElement.TYPE_TEXT then
                local textArr = segment[4]
                assert(#textArr>0,'segment text cannot be empty string!')
                local text = table.concat(textArr,'',segStartIndex,segEndIndex)
                local font = element[3]
                if font==nil or font=='' then 
                    font = DEFAULT_TTF_PATH
                end
                local fontSize = element[4] or 20
                local charWidth = 0
                local charHeight = 0
                local lb = nil
                local key = nil
                if text == '\n' then
                    --assert(x==0,'line-breaker must be the first in one line!')
                    h = math.max(h,fontSize)
                    charWidth = 0
                    charHeight = fontSize
                    touchID = NONE_TOUCH
                else
                    local size = nil
                    local outlineSize = element[7] or 0
                    if text == ' ' or text == '\t'then
                        --不显示空格和制表符
                        touchID = NONE_TOUCH
                        charWidth,charHeight = charWidths[segStartIndex],charHeights[segStartIndex]-- UBBUtil.getCharSize(font,fontSize,' ',outlineSize,sX,sY)
                    else
                        for i = segStartIndex,segEndIndex do
                            local cW,cH = charWidths[i],charHeights[i]
                            charHeight = math.max(charHeight,cH)
                            charWidth = charWidth+cW
                        end
                        local color = element[5] or 0xffffffff
                        local outlineColor = element[6] or 0xffffffff
                        
                        key,lb = UBBCache.getLabelFromPool(nil,font,fontSize,color,outlineColor,outlineSize,text,sX,sY,new_label_pool,self.label_pool_in_use,true,self.content_layer)
                        if lb then
                            newList[lb] = true
                            size = lb:getContentSize()
                        else
                            lb = LAZY_PLACE
                            table.insert(lazyNodes,{key,font,fontSize,color,outlineColor,outlineSize,text,sX,sY})
                        end
                    end
                end
                --更新高度和位置
                x = x + charWidth
                h = math.max(h,charHeight)
                table.insert(nodes,{lb,charWidth,charHeight,pAlign,lineAlign,touchID})
            elseif eType == UBBElement.TYPE_GROUP or eType == UBBElement.TYPE_RES then
                local groupElements
                if eType == UBBElement.TYPE_RES then
                    segStartIndex = 1
                    segEndIndex = 1
                    groupElements = {element}
                else
                    groupElements = element[2]
                end
                for i = segStartIndex,segEndIndex do
                    local scheme = groupElements[i][2]
                    local url = groupElements[i][3]
                    local params = groupElements[i][4] or {}
                    local eW,eH = charWidths[i],charHeights[i]
                    local eNode = UBBCache.getResFromPool(scheme,url,params,sX,sY,new_res_pool,self.res_pool_in_use,true,self.content_layer)
                    if eNode then
                        newList[eNode] = true
                    else
                        eNode = LAZY_PLACE_RES
                        table.insert(lazyNodes,{scheme,url,params,sX,sY})
                    end
                    table.insert(nodes,{eNode,eW,eH,pAlign,lineAlign,touchID})
                    --更新该行高度和位置
                    h = math.max(h,eH)
                    x = x + eW                    
                end
            elseif eType == UBBElement.TYPE_PLACEHOLDER then
                local eW = element[2]
                assert(eW and eW>0,'placeholder must have width!')
                local eH = element[3] or 0
                h = math.max(h,eH)
                x = x + eW
                table.insert(nodes,{nil,eW,eH,pAlign,lineAlign,nil})
            else
                assert(false,'invalid element type!')
            end
        end
        table.insert(widths,x)
        table.insert(heights,h)
        table.insert(lineNodes,nodes)
        totalH = totalH + h
        maxW = math.max(maxW,x)
    end


    --延后加载
    local i = 1
    for _,nodes in ipairs(lineNodes)do
        for _,nodeArr in ipairs(nodes)do
            if nodeArr[1] == LAZY_PLACE then
                local arr = lazyNodes[i]
                i = i+1
                local _,lb = UBBCache.getLabelFromPool(arr[1],arr[2],arr[3],arr[4],arr[5],arr[6],arr[7],arr[8],arr[9],new_label_pool,self.label_pool_in_use,false,self.content_layer)
                assert(lb,'must have valid label object!')
                nodeArr[1] = lb
                newList[lb] = true
            elseif nodeArr[1] == LAZY_PLACE_RES then
                local arr = lazyNodes[i]
                i = i+1
                local eNode = UBBCache.getResFromPool(arr[1],arr[2],arr[3],arr[4],arr[5],new_res_pool,self.res_pool_in_use,false,self.content_layer)
                nodeArr[1] = eNode
                newList[eNode] = true
            end
        end
    end

    --需要重新对所有元素进行显示顺序排序吗？如果需要，此处需要添加排序方法 zjb 2015年5月22日

    self.heights = heights --每一行的总长度
    self.widths = widths --每一行的总宽度
    self.lineNodes = lineNodes --每一行的node数组
    self.totalH = totalH--总高度
    self.maxW = maxW--最大宽度

    
    --移走没有用的对象
    if not DEBUG_NOCACHE then
        UBBCache.moveUnusedObjToPool(self.label_pool_in_use,self.res_pool_in_use,self.content_layer)
    end
    for _,oldObject in ipairs(oldList)do
        if not newList[oldObject] and oldObject:getParent()then
            self.content_layer:removeChild(oldObject)
        end
    end
    self.label_pool_in_use = new_label_pool
    self.res_pool_in_use = new_res_pool

    
    

end

--根据对齐参数和显示数据调整各个node的位置
function UBBLabel:adjustPosition()
    if not self.heights then return end
    local heights = self.heights --每一行的总长度
    local widths = self.widths --每一行的总宽度
    local lineNodes = self.lineNodes --每一行的node数组
    local totalH = self.totalH --总高度

    self.lastXX = self.lineNodes

    if DEBUG_VERSION then
        self.DEBUG_layer:removeAllChildren()
    end

    --根据对齐参数位置校准
    local offsetY = 0
    if self.height then
        --计算总y偏移
        if self.vAlign == cc.VERTICAL_TEXT_ALIGNMENT_TOP then
            offsetY = self.height/2
        elseif self.vAlign == cc.VERTICAL_TEXT_ALIGNMENT_CENTER then
            offsetY = totalH/2
        elseif self.vAlign == cc.VERTICAL_TEXT_ALIGNMENT_BOTTOM then
            offsetY = self.height/2-(self.height-totalH)
        end
    end
    local lY = 0
    local lineSegTouchArray = {}
    for i,nodes in ipairs(lineNodes)do
        local h = heights[i]
        local segTouchArray = {lY+offsetY,lY+offsetY-h}
        lY = lY - h
        local totalW = widths[i]

        local x = 0
        for _,tmp in ipairs(nodes)do
            local node = tmp[1]
            local eW = tmp[2]
            local eH = tmp[3]
            local pAlign = tmp[4] or self.hAlign
            local lineAlign = tmp[5] or cc.VERTICAL_TEXT_ALIGNMENT_BOTTOM
            local touchID = tmp[6] or NONE_TOUCH
            local lineOffsetY = 0
            local offsetX = 0
            if pAlign == cc.TEXT_ALIGNMENT_LEFT then
                offsetX = -self.width/2
            elseif pAlign == cc.TEXT_ALIGNMENT_CENTER then
                offsetX = -totalW/2
            elseif pAlign == cc.TEXT_ALIGNMENT_RIGHT then
                offsetX = -self.width/2+(self.width-totalW)
            end
            local nodeX = x+offsetX
            local nodeY = lY+offsetY

            if node then
                if lineAlign == cc.VERTICAL_TEXT_ALIGNMENT_TOP then
                    lineOffsetY =(h-eH)
                elseif lineAlign == cc.VERTICAL_TEXT_ALIGNMENT_CENTER then
                    lineOffsetY =(h-eH)/2
                end
                nodeY = nodeY + lineOffsetY
                node:setPositionX(nodeX)
                node:setPositionY(nodeY)
                --设置锚点，让坐标朝段落的左下角对齐
                node:setAnchorPoint(_ANCHOR_POINT)
                table.insert(segTouchArray,{nodeX,nodeY,nodeX+eW,nodeY+eH,touchID})
            end
            if DEBUG_DRAW_SEGMENT then
                local rnode
                if node then
                    rnode = createRectNode(eW,eH,touchID and 0xffff8080 or 0x2f00ffff,0.5,nil,false,true)
                else
                    rnode = createRectNode(eW,eH,0x8fffafaf,0.5,nil,false,false)
                end
                rnode:setPositionX(nodeX)
                rnode:setPositionY(nodeY)
                self.DEBUG_layer:addChild(rnode)
            end
            x = x+eW
        end
        table.insert(lineSegTouchArray,segTouchArray)
    end
    self.lineSegTouchArray = lineSegTouchArray


    if DEBUG_DRAW_BORDER then
        if self.height then
            self.DEBUG_layer:addChild(createRectNode(self.width,self.height,0x20ff0000,0.5,nil,true,true))
        else
            local rn = createRectNode(self.width,totalH,0x2000ff00,0.5,nil,true,true)
            self.DEBUG_layer:addChild(rn)
            rn:setPositionY(-totalH/2)
        end
    end    
end







--endregion
